﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FitNesseRunner;
using FitNesseRunner.Model;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FitNesseRunnerTests
{
	[TestClass]
	public class QueryTableHandlerShould
	{
		// TODO: organize IReporter and its usages

		private static FakeReflectionProvider _reflectionProvider;
		private static FakeReporter _reporter;
		private static ScriptTableHandlerShould.FakeContext _context;

		[TestInitialize]
		public void Initialize()
		{
			_reflectionProvider = new FakeReflectionProvider();
			_reporter = new FakeReporter();
			_context = new ScriptTableHandlerShould.FakeContext(_reflectionProvider);
		}

		[TestMethod]
		public void HandleTablesWithQueryPrefix()
		{
			var table = new FakeTable(new[] {"query:something"});
			var queryHandler = CreateQueryTableHandler();
			Assert.IsTrue(queryHandler.CanHandle(table));
		}

		[TestMethod]
		public void NotHandleTablesWithoutQueryPrefix()
		{
			var table = new FakeTable(new[] { "something" });
			var queryHandler = CreateQueryTableHandler();
			Assert.IsFalse(queryHandler.CanHandle(table));
		}

		[TestMethod]
		public void InstantiateClassAccordingToNameAfterPrefix()
		{
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr"});
			Assert.AreEqual("Something", _reflectionProvider.InstantiatedClassName);
		}

		[TestMethod]
		public void ConvertClassnameToPascalCaseBeforeInstantiating()
		{
			HandleQueryTable(new[] { "query:this is a long name" },
							 new[] { "hdr" });
			Assert.AreEqual("ThisIsALongName", _reflectionProvider.InstantiatedClassName);
		}

		[TestMethod]
		public void PassArgumentsToConstructor()
		{
			HandleQueryTable(new[] {"query:something", "arg1", "arg2"},
			                 new[] {"hdr"});
			CollectionAssertEx.AreEqual(new[] {"arg1", "arg2"}, _reflectionProvider.ConstructorArguments);
		}

		[TestMethod]
		public void ResolveVariablesOnConstructorArguments()
		{
			_context.Variables.Add("x", "value of x");
			HandleQueryTable(new[] { "query:something", "arg1", "$x" },
							 new[] { "hdr" });
			CollectionAssertEx.AreEqual(new[] { "arg1", "value of x" }, _reflectionProvider.ConstructorArguments);
		}

		[TestMethod]
		public void CallQueryMethodOnConstructedInstance()
		{
			_reflectionProvider.InstanceToReturn = new object();
			_reflectionProvider.MethodToInvoke = () => null;

			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr"});

			Assert.AreEqual("Query", _reflectionProvider.InvokedMethodName);
			CollectionAssertEx.IsEmpty(_reflectionProvider.ParametersToInvokedMethod);
			Assert.AreSame(_reflectionProvider.InstanceToReturn, _reflectionProvider.InstanceForInvokedMethod);
		}

		[TestMethod]
		public void ReportConstructorInvoked()
		{
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr"});

			Assert.AreEqual("Something", _reporter.ConstructedClassName);
		}

		[TestMethod]
		public void ReportFailedToConstructIfContructorThrewException()
		{
			var expectedException = TestConstructorException();

			Assert.AreEqual("Something", _reporter.FailedToConstructClassName);
			Assert.AreEqual(expectedException, _reporter.ConstructorException);
		}

		[TestMethod]
		public void ReportConstructingEvenIfConstructorThrewException()
		{
			TestConstructorException();
			Assert.AreEqual("Something", _reporter.ConstructedClassName);
		}

		[TestMethod]
		public void NotTryToInvokeQueryMethodIfConstructorFailed()
		{
			TestConstructorException();
			Assert.IsNull(_reflectionProvider.InvokedMethodName);
		}

		[TestMethod]
		public void ReportTestErrorIfTableContainsOnlyOneRow()
		{
			HandleQueryTable(new[] {"query:something"});
			Assert.AreEqual("Query table must have at least 2 rows", _reporter.TestErrorMessage);
		}

		[TestMethod]
		public void ReportErrorIfQueryMethodReturnsIncorrectType()
		{
			TestQueryMethodReturnsIncorrectType("hello");
			TestQueryMethodReturnsIncorrectType(null);
			TestQueryMethodReturnsIncorrectType(new List<object>());
		}

		[TestMethod]
		public void ReportExceptionInQueryMethod()
		{
			var exception = new Exception("My exception");
			_reflectionProvider.MethodToInvoke = () => { throw exception; };
			_reflectionProvider.InstanceToReturn = new object();

			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr"});

			Assert.AreEqual(typeof(object).Name, _reporter.InvocationFailedClassName);
			Assert.AreEqual("Query", _reporter.InvocationFailedMethodName);
			CollectionAssertEx.AreEqual(Enumerable.Empty<string>(), _reporter.InvocationFailedParameters);
			Assert.AreEqual(exception, _reporter.InvocationFailedException);
		}

		[TestMethod]
		public void ReportMissingRow()
		{
			SetQueryResult();
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr1"},
			                 new[] {"row1"});

			AssertMissingRow(_reporter.FailedParametersList.Single(), "row1");
		}

		[TestMethod]
		public void ReportMultipleMissingRows()
		{
			SetQueryResult();
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr1"},
			                 new[] {"row1"},
			                 new[] {"row2"});

			AssertMissingRow(_reporter.FailedParametersList.ElementAt(0), "row1");
			AssertMissingRow(_reporter.FailedParametersList.ElementAt(1), "row2");
		}

		[TestMethod]
		public void ReportFoundRow()
		{
			SetQueryResult(new[] {new object[] {"hdr1", "row1"}});
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr1"},
			                 new[] {"row1"});

			CollectionAssertEx.Contains("Row found with hdr1=row1", _reporter.PassedMessages);
		}

		[TestMethod]
		public void ReportTestErrorIfRowsHaveMissingCells()
		{
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr1", "hdr2"},
			                 new[] {"row1"});

			Assert.AreEqual("Rows have missing cells", _reporter.TestErrorMessage);
		}

		[TestMethod]
		public void ReportNonPresentFields()
		{
			SetQueryResult(new[] { new object[] { "hdr1", "1.1" } });
			HandleQueryTable(new[] { "query:something" },
							 new[] { "hdr1", "hdr2" },
							 new[] { "1.1", "1.2" });

			AssertMissingField("1.1", "hdr2", "1.2", _reporter.FailedParametersList.Single());
		}

		[TestMethod]
		public void ReportMismatchedFields()
		{
			SetQueryResult(new[]
				{
					new object[] {"hdr1", "1.1"},
					new object[] {"hdr2", "xxx"},
				});
			HandleQueryTable(new[] { "query:something" },
							 new[] { "hdr1", "hdr2" },
							 new[] { "1.1", "1.2" });

			AssertMismatchedField("1.1", "hdr2", "1.2", "xxx", _reporter.FailedParametersList.Single());
		}

		[TestMethod]
		public void ReportSuccessOnMatchedFields()
		{
			SetQueryResult(new[]
		        {
		            new object[] {"hdr1", "1.1"},
		            new object[] {"hdr2", "1.2"},
		        });
			HandleQueryTable(new[] { "query:something" },
							 new[] { "hdr1", "hdr2" },
							 new[] { "1.1", "1.2" });

			CollectionAssertEx.Contains("Row id='1.1', field='hdr1', value='1.1'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='1.1', field='hdr2', value='1.2'", _reporter.PassedMessages);
		}

		[TestMethod]
		public void CompareNonStringValues()
		{
			SetQueryResult(new[]
		        {
		            new object[] {"hdr1", 1.1},
		            new object[] {"hdr2", 1.2},
		        });
			HandleQueryTable(new[] { "query:something" },
							 new[] { "hdr1", "hdr2" },
							 new[] { "1.1", "1.2" });

			CollectionAssertEx.Contains("Row id='1.1', field='hdr1', value='1.1'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='1.1', field='hdr2', value='1.2'", _reporter.PassedMessages);
		}

		[TestMethod]
		public void CellsShouldBeIdentifiedByHeadersAndNotByIndex()
		{
			SetQueryResult(new[]
				{
					new object[] {"hdr3", 1.3},
					new object[] {"hdr2", 1.2},
					new object[] {"hdr1", 1.1},
				},
			               new[]
			               	{

			               		new object[] {"hdr2", "2.2"},
			               		new object[] {"hdr3", "2.3"},
			               		new object[] {"hdr1", "2.1"},
			               	});

			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr1", "hdr2", "hdr3"},
			                 new[] {"1.1", "1.2", "1.3"},
			                 new[] {"2.1", "2.2", "2.3"});

			CollectionAssertEx.IsEmpty(_reporter.FailedParametersList, "No failures expected");
			CollectionAssertEx.Contains("Row id='1.1', field='hdr1', value='1.1'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='1.1', field='hdr2', value='1.2'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='1.1', field='hdr3', value='1.3'", _reporter.PassedMessages);

			CollectionAssertEx.Contains("Row id='2.1', field='hdr1', value='2.1'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='2.1', field='hdr2', value='2.2'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='2.1', field='hdr3', value='2.3'", _reporter.PassedMessages);
		}

		[TestMethod]
		public void TrimColumnHeaders()
		{
			SetQueryResult(new[]
		        {
		            new object[] {"hdr1", "1.1"},
		            new object[] {"hdr2", "1.2"},
		        });
			HandleQueryTable(new[] { "query:something" },
							 new[] { "hdr1         ", "hdr2           " },
							 new[] { "1.1", "1.2" });

			CollectionAssertEx.Contains("Row id='1.1', field='hdr1', value='1.1'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='1.1', field='hdr2', value='1.2'", _reporter.PassedMessages);
		}

		[TestMethod]
		public void TrimValues()
		{
			SetQueryResult(new[]
		        {
		            new object[] {"hdr1", "1.1"},
		            new object[] {"hdr2", "1.2"},
		        });
			HandleQueryTable(new[] { "query:something" },
							 new[] { "hdr1", "hdr2" },
							 new[] { "1.1          ", "1.2         " });

			CollectionAssertEx.Contains("Row id='1.1', field='hdr1', value='1.1'", _reporter.PassedMessages);
			CollectionAssertEx.Contains("Row id='1.1', field='hdr2', value='1.2'", _reporter.PassedMessages);
		}

		private static void AssertMismatchedField(string rowId, string fieldName, string expectedValue, string actualValue, FakeReporter.FailedParameters failedParameters)
		{
			Assert.AreEqual(_reflectionProvider.InstanceToReturn, failedParameters.Instance);
			Assert.AreEqual(string.Format("Query (row id={0}, field={1})", rowId, fieldName), failedParameters.MethodName);
			Assert.AreEqual(expectedValue, failedParameters.ExpectedValue);
			Assert.AreEqual(actualValue, failedParameters.ActualValue);
		}

		private static void AssertMissingField(string rowId, string fieldName, string expectedValue, FakeReporter.FailedParameters failedParameters)
		{
			AssertMismatchedField(rowId, fieldName, expectedValue, "(field not present)", failedParameters);
		}

		private static void SetQueryResult(params object[][][] queryResult)
		{
			_reflectionProvider.InstanceToReturn = new object();
			var queryResultList = new List<List<List<object>>>(
				from row in queryResult
				let cells = from cell in row
				            select cell.ToList()
				select cells.ToList());
			_reflectionProvider.MethodToInvoke = () => queryResultList;
		}

		private static void AssertMissingRow(FakeReporter.FailedParameters failedParameters, string expected)
		{
			Assert.AreEqual(_reflectionProvider.InstanceToReturn, failedParameters.Instance);
			Assert.AreEqual("Query", failedParameters.MethodName);
			Assert.AreEqual(expected, failedParameters.ExpectedValue);
			Assert.AreEqual("(missing)", failedParameters.ActualValue);
		}

		private static void HandleQueryTable(params string[][] cells)
		{
			var table = new FakeTable(cells);
			var queryHandler = CreateQueryTableHandler();
			queryHandler.Handle(table);
		}

		private static void TestQueryMethodReturnsIncorrectType(object returnValue)
		{
			_reflectionProvider.InstanceToReturn = new object();
			_reflectionProvider.MethodToInvoke = () => returnValue;
			
			HandleQueryTable(new[] {"query:something"},
			                          new[] {"hdr"});

			Assert.AreEqual("Query method must return List<List<List<object>>>", _reporter.InvocationErrorMessage);
			Assert.AreEqual(typeof(object).Name, _reporter.InvocationErrorClassName);
			Assert.AreEqual("Query", _reporter.InvacationErrorMethodName);
			CollectionAssertEx.IsEmpty(_reporter.InvacationErrorParameters);
		}

		private static Exception TestConstructorException()
		{
			var expectedException = new Exception("Dummy exception");
			_reflectionProvider.ThrowContructorException(expectedException);
			
			HandleQueryTable(new[] {"query:something"},
			                 new[] {"hdr"});

			return expectedException;
		}

		private static ITableHandler CreateQueryTableHandler()
		{
			return new QueryTableHandler(_reporter, _context);
		}

		public class FakeReporter : IReporter
		{
			public FakeReporter()
			{
				FailedParametersList = new List<FailedParameters>();
				PassedMessages = new List<string>();
			}

			public void Pass(string message, params object[] args)
			{
				PassedMessages.Add(string.Format(message, args));
			}

			public void Invoking(string className, string methodName, IEnumerable<string> parameterValues)
			{
				throw new NotImplementedException();
			}

			public void InvocationFailed(string className, string methodName, IEnumerable<string> parameterValues, Exception exception)
			{
				InvocationFailedClassName = className;
				InvocationFailedMethodName = methodName;
				InvocationFailedParameters = parameterValues;
				InvocationFailedException = exception;
			}

			public void Constructing(string className)
			{
				ConstructedClassName = className;
			}

			public void Failed(object instance, string methodName, object expectedValue, object actualValue)
			{
				FailedParametersList.Add(new FailedParameters(instance, methodName, expectedValue, actualValue));
			}

			public void RunTest(string path)
			{
				throw new NotImplementedException();
			}

			public void FailedToConstruct(string className, Exception ex)
			{
				FailedToConstructClassName = className;
				ConstructorException = ex;
			}

			public void Invoking(PropertyInfo propertyInfo, string stringValue)
			{
				throw new NotImplementedException();
			}

			public void Show(string methodName, string result)
			{
				throw new NotImplementedException();
			}

			public void TestError(string message)
			{
				TestErrorMessage = message;
			}

			public void InvocationError(string message, Type objectType, string methodName, IEnumerable<string> parameters)
			{
				InvocationErrorMessage = message;
				InvocationErrorClassName = objectType.Name;
				InvacationErrorMethodName = methodName;
				InvacationErrorParameters = parameters;
			}

			public string ConstructedClassName { get; private set; }

			public string FailedToConstructClassName { get; private set; }

			public Exception ConstructorException { get; private set; }

			public string TestErrorMessage { get; private set; }

			public string InvocationErrorMessage { get; private set; }

			public IEnumerable<object> InvacationErrorParameters { get; private set; }

			public string InvacationErrorMethodName { get; private set; }

			public string InvocationErrorClassName { get; private set; }

			public string InvocationFailedClassName { get; private set; }

			public Exception InvocationFailedException { get; private set; }

			public IEnumerable<object> InvocationFailedParameters { get; private set; }

			public string InvocationFailedMethodName { get; private set; }

			public List<FailedParameters> FailedParametersList { get; private set; }

			public List<string> PassedMessages { get; private set; }

			public class FailedParameters
			{
				public object Instance { get; private set; }
				public string MethodName { get; private set; }
				public object ExpectedValue { get; private set; }
				public object ActualValue { get; private set; }

				public FailedParameters(object instance, string methodName, object expectedValue, object actualValue)
				{
					Instance = instance;
					MethodName = methodName;
					ExpectedValue = expectedValue;
					ActualValue = actualValue;
				}
			}
		}

		public class FakeReflectionProvider : IReflectionProvider
		{
			private Exception _exceptionToThrowOnConstructor;
			public string InstantiatedClassName { get; private set; }
			public IEnumerable<string> ConstructorArguments { get; private set; }
			public string InvokedMethodName { get; private set; }
			public object InstanceForInvokedMethod { get; private set; }
			public IEnumerable<string> ParametersToInvokedMethod { get; private set; }
			public object InstanceToReturn { get; set; }

			public Func<object> MethodToInvoke { get; set; }

			public object CreateInstance(string className, IEnumerable<string> parameters)
			{
				InstantiatedClassName = className;
				ConstructorArguments = parameters;
				if (_exceptionToThrowOnConstructor != null)
					throw _exceptionToThrowOnConstructor;

				return InstanceToReturn;
			}

			public object Invoke(object instance, string methodName, IEnumerable<string> parameters)
			{
				InvokedMethodName = methodName;
				ParametersToInvokedMethod = parameters;
				InstanceForInvokedMethod = instance;
				return MethodToInvoke();
			}

			public void ThrowContructorException(Exception exception)
			{
				_exceptionToThrowOnConstructor = exception;
			}
		}

		public class FakeTable : ITable
		{
			private readonly string[][] _cells;

			public FakeTable(params string[][] cells)
			{
				_cells = cells;
			}

			public IEnumerable<IEnumerable<string>> Cells
			{
				get { return _cells; }
			}
		}
	}
}
